fix(plugin-kanban): ObjectKanbanComponentProps.schema names both registered node types (objectui#7322 item ②) - #8153
Conversation
…stered node types `ObjectKanbanRenderer` is registered under two keys — `'object-kanban'` and `'kanban'` — whose declared node types are `ObjectKanbanSchema` and `KanbanSchema`. The prop named `KanbanSchema` alone, so no `object-kanban` node was assignable to the component that renders it; the discriminants are disjoint literals, so half the boards this component serves had no cast-free annotation. Measured read set: `ObjectKanban` reads thirteen keys off `schema`; the two declarations together cover twelve, and each arm is load-bearing (`titleField` comes only from `ObjectKanbanSchema`, `columns` / `cardTitle` / `swimlaneField` / `grouping` only from `KanbanSchema`). `filter` is declared by neither and still rides `BaseSchema`'s index signature — reported, not changed here. The prop is now the union of the two arms. Three schema-key `as any` casts drop out of `ObjectKanban.tsx`; four of six in-package fixtures drop their `as never` for `satisfies ObjectKanbanSchema` (the two that remain are static boards with no `objectName`, objectui#7780). A new pin derives the registered key set from `index.tsx` off disk and requires it to equal the union's arms, and the objectui#7664 prop leg moves from identity to assignability — the form that ruling's own words claim, and the form its `kanban-ui` leg already uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Contract review — PASS — PR #8153 (objectui#7322 item ②), head Reviewed-by: ⭐ The union-vs-single-type question — re-measured by this seat, ⛔ not accepted from the reportThis was the dispatch's central clause. I warned that the card's implied remedy ("point the prop at
⇒ the union is provably the only honest type, and the card was wrong in both directions. The control row is what makes the other rows readings: a tabulation that answered "declared" for everything would show it here, and it does not — ① Derived judgmentsDirection: WIDENS
⭐⭐ The anti-vacuity pin is the best thing in this diff. Reverse verification at the package boundary — a widening is invisible unless the consumer reads the rebuilt ② semver
③ Boundary flags
VerdictPASS. ⛔ Not landing yet. CI unconverged: 32 enumerated against This PR carries the closing keyword — item ① landed as PR #7774 and item ② is the last item — so objectui#7322 closes on merge and this seat strips Generated by Claude Code |
|
Addendum to the review above — Two things in the PR body that the ⭐ The
⭐ And the union shape has a house precedent I had not named in the dispatch: objectui#7311 already landed Nothing in either changes the verdict: PASS, landing on convergence. Generated by Claude Code |
|
Landing — PR #8153 (objectui#7322 item ②), head Pre-landing three: ① Contract review PASS (comment 5562152707) at ② CI converged, enumerated 32 against The single failure is Identical to #8142's, #8145's and #8150's. Carded objectstack#16186; the red is by design per objectui#7689, whose triage ⛔ forbids reverting the pin to get green. ⛔ No re-run spent.
③ Ready flipped, auto-merge SQUASH enabled. ⭐ Landing control established BEFORE the queue takes it, so the post-merge confirmation is a reading and ⛔ not a hopeful grep:
⇒ the zero is a reading on a live file.
Generated by Claude Code |
Fixes #7322 (item ②; item ① landed as PR #7774 on 2026-09-05)
ObjectKanbanRendereris registered under two keys —'object-kanban'(index.tsx:416)and
'kanban'(:429), andkanban-plugin-dialect-authoritative-7664.test.tsalready pinsthat both resolve to the same renderer. The two keys have different declared node types:
typeliteralobjectName/groupBy'object-kanban'ObjectKanbanSchema(objectql.ts)'object-kanban''kanban'KanbanSchema(complex.ts, moved down by #7743)'kanban'ObjectKanbanComponentProps.schemanamedKanbanSchemaalone. The discriminants aredisjoint string literals, so no
object-kanbannode was assignable to the component thatrenders it, and the prop lied about half the nodes it serves.
The measurement that settled union-vs-single-type
The dispatch said measure first, choose second, and to stop and report if no single type can
serve both registrations. It can't — and the reason is not just the discriminant.
ObjectKanbanreads thirteen keys offschema. Neither declaration covers them; the twotogether cover twelve:
BaseSchemaKanbanSchemaObjectKanbanSchemaobjectName,groupBy,limit,cardFieldscolumns,cardTitle,swimlaneField,groupingtitleFielddata,bind,classNamedata,className)filterEach arm is load-bearing, in both directions:
ObjectKanbanSchemaalone (the original card's implied remedy) drops fourdeclared reads and the
'kanban'registration — it is wrong in the other direction;KanbanSchemaalone (the status quo) dropstitleField— which is exactly whyObjectKanban.tsxspelled that read(schema as any).titleField.⇒ the honest type is the union,
KanbanSchema | ObjectKanbanSchema. It claims exactly theaccept set the registry dispatches to this component and no more: an unregistered node type is
still turned away (pinned). This is the same land shape #7311 already took for the calendar
(
ObjectCalendarComponentProps.schema: ObjectGridSchema | CalendarSchema).filter(:310, the real$filteron the fetch) is declared by neither face and stillrides
BaseSchema's index signature. Measured and reported, not changed here — this cardmoves the prop, not the two published schema faces. See "Findings" below.
What the change costs the tree — the visible payoff
Five of the six in-package fixtures that mount an
object-kanbanboard were escaping the propwith
as never. Four of them now carry a realsatisfies ObjectKanbanSchema:ObjectKanban.rejectedMoveRollback.test.tsxObjectKanban.requiredWhenPrompt.test.tsxObjectKanban.markedRefusalToast.test.tsxObjectKanban.navWidthDefault.test.tsxThe other two (
overlayTitleI18n,overlayTitleNoProviderFallback) are static boards —columnsplus inlinedata, no fetch — so they author noobjectName, whichObjectKanbanSchemadeclares required. That requiredness is #7780's subject; their castsstay, now carrying the reason and the card number instead of being silent.
Three
as anycasts drop out ofObjectKanban.tsx:titleFieldat:350/:1024(honestnow that the
object-kanbanarm declares it) andcardFields/cardTitle(already declared;the casts were redundant). Measured with eslint on the same file before and after: 28 → 24
@typescript-eslint/no-explicit-anyfindings, i.e. exactly the fouranytokens removed.(schema as any).navigationat:789stays:navigationis declared on neither face, soremoving the cast would change the spelling of an index-signature read and nothing else — the
#5903 disposition, restated.
index.tsx:395'sschema: any— the "if and only if" answered NOThe dispatch authorised cleaning it up iff the measurement makes an honest type available
there. It does not, on two readings:
ComponentRegistry.register's component parameter isuntyped, so an annotation there is a claim about registry dispatch that nothing enforces; and
registration.test.tsx:32renders a deliberately minimal{ type: 'object-kanban' }, which nodeclared type accepts while #7780 is open. #5903 took the same disposition for the same reason
("the registered renderer still passes
schema: any, so no runtime shape is turned away").The pin, and the pin it amends
New —
packages/plugin-kanban/src/__tests__/object-kanban-component-props-7322.test.ts.Suite 1 (compile-time, read by
tsc -p tsconfig.test.json): both arms accepted, anunregistered node type (
ObjectGridSchema) refused, the discriminant equal to'kanban' | 'object-kanban', withIsAnycontrols so a widening toanycannot pass. Suite 2(runtime): the registered key set is extracted from
index.tsxoff disk and required toequal the union's arms — a hand-written pair of strings is exactly what this defect survived
under, so a third registration or a re-key goes red naming the key, and the fix is to move the
prop rather than the test. Anti-vacuity: the extraction must be non-empty, must not pick up
'kanban-ui'/'kanban-enhanced'(registered in the same file to other renderers), and everykey it finds must resolve to
ObjectKanbanRendererin the live registry.Amended —
kanban-plugin-dialect-authoritative-7664.test.ts, leg 3's first bullet, froman
Equalidentity assertion between the prop'sschemamember andKanbanSchema, toassignability. Identity was never what objectui#7664's ruling claimed; its words are that
"the four registered renderers' props still type-check against the declared schema" — the
same assignability form leg 3's own
kanban-uibullet already uses, and for the same reason.Identity was merely the shape the prop happened to have while it named one arm, which was
itself this card's defect.
Verification
All on
25907cd70; every exit code captured before any pipe; build/test through the sharedverify lock.
pnpm --filter @object-ui/plugin-kanban type-check(tsc --noEmit+tsconfig.test.json)error TSlinepnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/Test Files 25 passed (25) · Tests 130 passed (130)turbo run type-check --filter=@object-ui/runner --filter=@object-ui/app-shellTasks: 31 successful, 31 total(the two package-level consumers)pnpm --filter @object-ui/plugin-kanban lint✖ 121 problems (0 errors, 121 warnings); 0 new on touched filespnpm --filter @object-ui/plugin-kanban buildDeclaration files built in 4378msnode scripts/check-changeset-presence.mjs✅ 8 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-control-bytes.mjs✅ OK (scanned 6511 tracked text file(s))pnpm check:spec-symbols✅ no comment cites a key its spec symbol does not declarepnpm check:element-data-source-declarationOK — 13 gate-consuming file(s) checkedpnpm check:unreferenced-sourcesOK Every shipped source file … is reachablepnpm check:vi-mock-specifiers/check:vi-mock-inherit/check:self-import/check:phantom-deps✅pnpm check:sdui-registration-pins❌ No console build to weigh at apps/console/dist/assets— a prerequisite complaint, not a red gate. Narrowing declared: this diff adds and removes no registration, which is that gate's whole subject. CI runs it with a real console build.turbo ls --affectedfrom the merge-base names 8 packages; the two with a real source-leveldependency on this one (
runner,app-shell) are type-checked above, the rest are apps andexamples that reach this package only through plugin registration and are CI's.
Reverse verification at the package boundary
A widening is invisible unless the consumer really reads the rebuilt
.d.ts. A scratch probeassigning an
ObjectKanbanSchema-typed value into the prop'sschemamember, importingthrough
dist/index:.d.ts→ exit 0;dist/ObjectKanban.d.tsreverted to the single arm (mutation proven bymd5, restored and re-proven) → exit 2,
TS2322: Type 'ObjectKanbanSchema' is not assignable to type 'KanbanSchema'.So the green is a reading of this change, not of a cache.
Ablation — both halves of the new pin, red-first
Run from the committed state, mutation proven by blob hash on disk, restored under
trap … EXIT INT TERMwithgit checkout HEAD --andgit diff HEADempty afterwards. Nodistis on the pin's own resolution path (../ObjectKanbanand../indexare source imports;tsconfig.test.jsonsets"paths": {}only for the@object-ui/*workspace deps, which thischange does not touch).
Leg A — prop reverted to the single arm (the union member replaced by
schema: KanbanSchema;anchors counted 1→0 and 0→1, blob
48bd449d→fba3ea76).Predicted red on the two new type legs and on the four
satisfiesfixtures. Observed:tsc -p tsconfig.test.jsonexit 2, 6 errors —TS2344atobject-kanban-component-props-7322.test.ts(77,43)and(89,41)(_AcceptsTheObjectKanbanNode,_DiscriminantIsTheTwoKeys) plusTS2322on all four fixtures. One refinement on theprediction: the fixtures fail at the JSX prop assignment (
TS2322), not at thesatisfiesclause (
TS1360) — the literals still satisfyObjectKanbanSchema; it is the prop that stopsaccepting them, which is the more exact statement of the defect.
Leg B — one registration re-keyed (
⚠️ Leg B's
'kanban'→'kanban2'inindex.tsx; blob4e354382→c01425be). Predicted exactly one red assertion with the anti-vacuity legsstaying green. Observed: vitest exit 1,
Tests 1 failed | 3 passed (4),AssertionError: expected [ 'kanban2', 'object-kanban' ] to deeply equal [ 'kanban', 'object-kanban' ].grep -cecho lines were mangled by quoting inside the script's heredoc and printedno usable count; the mutation's arrival on disk is carried instead by the blob-hash inequality
and by the assertion message itself, which quotes the mutated key back. Two independent
readings, so the leg stands; the broken echo is a script defect, reported rather than papered
over.
Restore proven on both legs: blobs equal
HEADagain,git diff HEADempty.Findings — reported, not filed, to avoid a fourth card on this family
The dispatch flagged that #7772 / #7773 / #7780 already sit on the
object-kanbanfamily andthis card must not duplicate them. Two measurements fall inside cards that already exist:
filterandnavigationare declared on neither kanban face whileObjectKanbanreadsboth. finding(types,plugin-kanban): the ruled
KanbanSchemacarries three zero-read members (allowCollapse,cardTemplates,columnWidths) and the board reads an undeclaredtitleField— enforce-or-remove on the shape objectui#7664 declared #7742 is already the enforce-or-remove ledger card forKanbanSchema'sdeclared-vs-read set (it names the undeclared
titleFieldread), and plugin-kanban / plugin-calendar: the registrations declare nofilterinput while both renderers readschema.filterand the spec declares it — the html tier reportsunknown-propon a spec-declared key (objectui#6678 class) #7712 coversfilterfrom the registration-
inputsangle.navigationis named by neither; it is added as acomment on finding(types,plugin-kanban): the ruled
KanbanSchemacarries three zero-read members (allowCollapse,cardTemplates,columnWidths) and the board reads an undeclaredtitleField— enforce-or-remove on the shape objectui#7664 declared #7742 rather than as a new card.KanbanSchemacarries three zero-read members (allowCollapse,cardTemplates,columnWidths) and the board reads an undeclaredtitleField— enforce-or-remove on the shape objectui#7664 declared #7742's premise. ItstitleFieldhalf reads "the board reads anundeclared
titleField" — after this change that read is declared (on theobject-kanbanarm) and cast-free, while still undeclared on
KanbanSchema. Its taker should re-measurefrom this commit.
Not touched
The four view-level
groupFieldalias sites (normalize-list-view.ts,ListView.tsx,ObjectView.tsx),BaseSchema's index signature (#5155), andpackages/types— read-onlyhere, as dispatched (slot 3/5 is editing
complex.ts'sChatbotSchemaregion concurrently).Nothing under
content/docs/releases/.Governance
node scripts/check-governed-queue-guard.mjs --teston this diff's paths:✅ NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.Held draft with
needs:contract-reviewper Clause-② (this widens a member of a prop typeexported from a published package). ⛔ Does not enqueue while that label is on it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code